﻿// //   Copyright 2007-2011 Comdiv (F. Sadykov) - http://code.google.com/u/fagim.sadykov/
// //   Supported by Media Technology LTD 
// //    
// //   Licensed under the Apache License, Version 2.0 (the "License");
// //   you may not use this file except in compliance with the License.
// //   You may obtain a copy of the License at
// //    
// //        http://www.apache.org/licenses/LICENSE-2.0
// //    
// //   Unless required by applicable law or agreed to in writing, software
// //   distributed under the License is distributed on an "AS IS" BASIS,
// //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //   See the License for the specific language governing permissions and
// //   limitations under the License.
// //   
// //   MODIFICATIONS HAVE BEEN MADE TO THIS FILE

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Comdiv.QWeb.Events;
using Comdiv.QWeb.Logging;
using Comdiv.QWeb.Serialization.Attributes;

namespace Comdiv.QWeb.Files {
	public class FileNameResolver : IFileNameResolver {
		private static int _id;
		private readonly IDictionary<string, string[]> _cache = new Dictionary<string, string[]>();

		public FileNameResolver() {
			Id = _id++;
		}

		public int Id { get; private set; }

		public int CacheCount {
			get { return _cache.Count; }
		}

		public string Root { get; set; }

		#region IFileNameResolver Members

		public int Version { get; protected set; }

		public string Resolve(QWebContext context, FileResolveResultType type, bool existedOnly, string[] probepaths,
		                      string[] probefiles, ILogListener log = null) {
			return Resolve(context, type, existedOnly, probepaths, probefiles, false, log).FirstOrDefault();
		}

		public string[] ResolveAll(QWebContext context, FileResolveResultType type, string[] probepaths,
		                           string[] probefiles, ILogListener log = null) {
			return Resolve(context, type, true, probepaths, probefiles, true, log);
		}

		public DateTime Timestamp { get; protected set; }

		public void ClearCache() {
			_cache.Clear();
		}

		public void SetRegistry(QWebServiceRegistry registry) {
			Registry = registry;
			registry.Events.Add(new FileNameResolver_Reseter(this));
		}

		public QWebServiceRegistry Registry { get; set; }

		#endregion

		protected string[] Resolve(QWebContext context, FileResolveResultType type, bool existedOnly, string[] probepaths,
		                           string[] probefiles, bool all, ILogListener log = null) {
			lock (this) {
				context = context ?? QWebContext.Current ?? new QWebContext("http://local/stub/");
				log = log ?? (Registry == null ? StubLogListener.Default : Registry.Log);
				if (null == probefiles || 0 == probefiles.Length) throw new ArgumentException("empty probe files");
				if (null != probefiles.FirstOrDefault(x => x.Contains("..")))
					throw new ArgumentException("cannot use .. path modifiers in files");
				probefiles = probefiles.Select(FileNameResolverExtensions.NormalizePath).ToArray();
				if (null == probepaths) {
					var ext = Path.GetExtension(probefiles.First());
					probepaths = FileNameResolverExtensions.GetDefaultProbesPaths(ext);
				}
				else {
					if (null != probepaths.FirstOrDefault(x => x.Contains("..")))
						throw new ArgumentException("cannot use .. path modifiers in dirs");
					probepaths = probepaths.Select(FileNameResolverExtensions.NormalizePath).ToArray();
				}

				var key = string.Format("{0}_{1}_{2}_{3}_{4}_{5}_{6}",
				                        context.RootDirectory.NormalizePath(),
				                        context.ApplicationName,
				                        type,
				                        existedOnly,
				                        probepaths.Aggregate((x, y) => x + "_" + y),
				                        probefiles.Aggregate((x, y) => x + "_" + y),
				                        all
					);
				log.trace("start resolving files with key " + key, context);
				string[] result;
				if (!_cache.ContainsKey(key)) {
					log.debug("no cached Context found", context);
					result = internalResolve(context, type, existedOnly, probepaths, probefiles, all, log);
					Version++;
					Timestamp = DateTime.Now;
					_cache[key] = result;
				}
				result = _cache[key];
				log.trace("resolution finished with " + result, context);
				return result;
			}
		}

		private string[] internalResolve(QWebContext context, FileResolveResultType type, bool existedOnly,
		                                 string[] probepaths, string[] probefiles, bool all, ILogListener log) {
			if (!existedOnly) return new[] {resolveBestPosiblePath(context, type, probepaths[0], probefiles[0], log)};
			var root = Root ?? context.RootDirectory.NormalizePath();
			var result = new List<string>();
			if (all) {
				foreach (var probefile in probefiles) {
					foreach (var dir in probepaths) {
						log.debug("enter dir " + dir, context);
						var path = (root + "/" + dir).NormalizePath();
						var pf = probefile;
						if (pf.StartsWith("/")) pf = pf.Substring(1);
						var full = Path.Combine(path, pf);
						var dir_ = Path.GetDirectoryName(full);
						var file_ = Path.GetFileName(full);
						if (Directory.Exists(dir_)) {
							foreach (var file in Directory.GetFiles(dir_, file_)) {
								result.Add(adaptFilePath(context, type, file));
							}
						}
					}
				}
			}
			else {
				foreach (var probefile in probefiles) {
					if (0 != result.Count) break;
					if (probefile.StartsWith("~/")) {
						var path = Path.Combine(root, probefile.Substring(2)).NormalizePath();
						log.debug("try " + path, context);
						if (File.Exists(path) || Directory.Exists(path)) {
							log.debug("existed", context);
							result.Add(path);
							break;
						}
					}
					foreach (var dir in probepaths) {
						log.debug("enter dir " + dir, context);
						var path = (root + "/" + dir).NormalizePath();
						var probe = (path + "/" + probefile).NormalizePath();
						log.debug("try " + probe, context);
						if (File.Exists(probe) || Directory.Exists(probe)) {
							log.debug("existed", context);
							result.Add(adaptFilePath(context, type, probe));
							break;
						}
					}
				}
			}
			return result.ToArray();
		}

		private string resolveBestPosiblePath(QWebContext context, FileResolveResultType type, string dir, string file,
		                                      ILogListener log) {
			var root = context.RootDirectory.NormalizePath();
			if (null == root) return null;
			if (file.StartsWith("~/")) {
				return Path.Combine(root, file.Substring(2)).NormalizePath();
			}
			var path = "/" + dir + "/" + file;
			var resolved = (root + path).NormalizePath();
			var result = adaptFilePath(context, type, resolved);
			log.debug("resolved to best possible  " + result, context);
			return result;
		}

		private string adaptFilePath(QWebContext context, FileResolveResultType type, string resolved) {
			var root = context.RootDirectory.NormalizePath();

			switch (type) {
				case FileResolveResultType.FullPath:
					return resolved.NormalizePath();
				case FileResolveResultType.LocalPath:
					return resolved.Substring(root.Length);
				case FileResolveResultType.FullUrl:
					return
						new Uri(new Uri(context.Uri.GetLeftPart(UriPartial.Authority)),
						        new Uri(("/" + context.ApplicationName + "/" + resolved.Substring(root.Length)).NormalizePath())).ToString
							().
							Replace("file:/", "").NormalizePath();
				case FileResolveResultType.LocalUrl:
					return
						("/" + context.ApplicationName + "/" + resolved.Substring(root.Length)).NormalizePath();
				default:
					throw new Exception("cannot define result for type " + type);
			}
		}

		#region Nested type: FileNameResolver_Reseter

		[Serialize]
		private class FileNameResolver_Reseter : QWebEventHandlerBase<ResetEvent> {
			public readonly string ResetOption = "files.resolver";
			public readonly string Type = "FileNameResolver_Reseter";
			private readonly FileNameResolver resolver;

			public FileNameResolver_Reseter(FileNameResolver resolver) {
				this.resolver = resolver;
			}

			public int Id {
				get { return resolver.Id; }
			}

			public int Version {
				get { return resolver.Version; }
			}

			public int CacheCount {
				get { return resolver.CacheCount; }
			}

			public override void Process(ResetEvent e) {
				if (e.Data.IsSet(ResetOption)) {
					resolver.ClearCache();
					e.Result.InvokeList.Add(this);
				}
			}
		}

		#endregion
	}
}